package il.ac.tau.arielgue.outxing.agents;

import il.ac.tau.arielgue.outxing.utils.Params;
import proevolution.GenePool;
import proevolution.GenePoolImpl;
import proevolution.agents.AbstractOrganism;
import proevolution.agents.Organism;
import repast.simphony.random.RandomHelper;
import simphony.util.messages.MessageCenter;
import cern.jet.random.Poisson;

/**
 * 
 * @author arielgue
 * 
 */
public abstract class OutXingOrganism extends AbstractOrganism {
	private static final long serialVersionUID = -8782854312671920977L;
	protected static final MessageCenter logger = MessageCenter
			.getMessageCenter(OutXingOrganism.class);
	private static double RECOMBINATION_FREQUENCY = 0.5;

	/**
	 * A is the normal allele, B is the deleterious allele (for selection loci)
	 * for mating MODIFIER loci A is uniform allele and B is the FAO allele
	 */
	public static final int HOMOZYGOT_AA = 0;
	public static final int HOMOZYGOT_BB = 1;
	// public static final int HOMOZYGOT_CC = 2;
	public static final int HETEROZYGOT_AB = 2;
	public static final int HETEROZYGOT_BA = 3;
	// public static final int HETEROZYGOT_AC = 5;
	// public static final int HETEROZYGOT_CA = 6;
	// public static final int HETEROZYGOT_BC = 7;
	// public static final int HETEROZYGOT_CB = 8;

	// TODO: check and test and document!!!
	public static int[][] MUTATION_MATRIX = null;
	static {
		MUTATION_MATRIX = new int[4][2];
		MUTATION_MATRIX[0][0] = 3;
		MUTATION_MATRIX[0][1] = 2;
		MUTATION_MATRIX[1][0] = 1;
		MUTATION_MATRIX[2][0] = 1;
		MUTATION_MATRIX[3][0] = 3;
		MUTATION_MATRIX[1][1] = 1;
		MUTATION_MATRIX[2][1] = 2;
		MUTATION_MATRIX[3][1] = 1;
	}

	private int outXingModifier = HOMOZYGOT_AA; // no outcrossing homozygot
	private int male = 0; // 0 is female, 1 is male
	private double fitness = Double.NaN;
	private double fitnessModifier = 1;
	private int movedToPool = 0;

	public OutXingOrganism() {
		super();
	}

	public OutXingOrganism(int numOfGenes) {
		super();
	}

	public OutXingOrganism(int[] alleles) {
		super();
	}

	public OutXingOrganism(OutXingOrganism other) {
		super(other);
		this.setOutXingModifier(other.getOutXingModifier());
		this.setMale(RandomHelper.nextIntFromTo(0, 1)); // TODO move to sexual
		// organism
	}

	@Override
	public double getFitness() {
		// fitness does not change during an organism's life, therefore it is
		// calculated only once
		if (Double.isNaN(fitness)) {
			fitness = 1;
			for (int gene = 0; gene < getAlleles().length; gene++) {
				int allele = getAllele(gene);
				double s = 1;
				if (allele == HOMOZYGOT_AA) {
					s = getSelectionModifierForAllele(gene,
							GenePoolImpl.HOUSEKEEPING_NORMAL_ALLELE);
				} else {
					s = getSelectionModifierForAllele(gene,
							GenePoolImpl.HOUSEKEEPING_DELETIRIOUS_ALLELE);
					// HETEROZYGOT
					if (allele != HOMOZYGOT_BB) {
						s *= getHeterozygotCoefficient();
					}
				}
				fitness *= (1 - s);
			}
		}
		return fitness * fitnessModifier;
	}

	/**
	 * 
	 * in xingulation there are no back mutations from deleterious to neutral
	 */
	@Override
	public void mutate() {

		// randomly select one gene
		int gene = RandomHelper.nextIntFromTo(0, getAlleles().length - 1);

		// get the current allele number
		int currentAllele = getAllele(gene);
		int newAllele = -1;
		if (MUTATION_MATRIX[currentAllele].length > 1) {
			// randomize the new allele
			int rand = RandomHelper.nextIntFromTo(0,
					MUTATION_MATRIX[currentAllele].length - 1);

			// DEBUG method
			// if (DateUtils.getCurrentTick()==300 && getAllele(gene)>0 && rand
			// >0){
			// System.out.println("stopped for debugging egarding mutations");
			// }

			newAllele = MUTATION_MATRIX[currentAllele][rand];
		} else {
			newAllele = MUTATION_MATRIX[currentAllele][0];
		}
		if (newAllele != currentAllele) {
			// set and log...
			setAllele(gene, newAllele);
			logger.debug("Mutation at organism " + getID() + " gene " + gene
					+ " mutated from " + currentAllele + " to " + newAllele);
		} else {
			logger.debug("New allele equals current allele at organism "
					+ getID() + " gene " + gene + ", allele " + currentAllele);
		}
	}

	/**
	 * 
	 * @param modifier
	 *            must be between 0 and 1.
	 * @return
	 */
	public double modifyFitness(double modifier) {
		if (modifier > 1 || modifier < 0) {
			throw new IllegalArgumentException(
					"Modifier to fitness must be between 0 and 1, but it is "
							+ modifier + " at organism " + getID());
		}
		return fitnessModifier *= modifier;
	}

	@Override
	protected double getSelectionModifierForAllele(int gene, int allele) {
		/*GenePool gp = ; DEBUG CODE
		if (gp == null)
			throw new NullPointerException("Gene pool can't be Null!");*/
		return getGenePool().getSelectionModifier(gene, allele);
	}

	public Organism reproduce(OutXingOrganism mate) {
		OutXingOrganism child = null;

		// generate 'outXing' genotype
		int modifierL = createGamete(new int[] { this.getOutXingModifier() })[0];
		int modifierR = createGamete(new int[] { mate.getOutXingModifier() })[0];
		if (RandomHelper.nextDouble() < 0.5) {
			// randomly mix the alleles as we put modifierL as the left allele.
			int tmp = modifierL;
			modifierL = modifierR;
			modifierR = tmp;
		}

		// AA(0) / BB(1)
		if (modifierL == modifierR) {
			if (modifierL == HOMOZYGOT_AA) {
				child = UniformOrganism.create(this);

			} else {
				child = FAOOrganism.create(this);
			}
			child.setOutXingModifier(modifierL);
		} else {
			// AB
			if (modifierL == HOMOZYGOT_AA) {
				child = FAOOrganism.create(this);
				child.setOutXingModifier(HETEROZYGOT_AB);
			}
			// BA
			else {
				child = FAOOrganism.create(this);
				child.setOutXingModifier(HETEROZYGOT_BA);
			}
		}

		// generate alleles - TODO move to 2 allele diploid organism
		int[] gameteL = this.getGamete();
		int[] gameteR = mate.getGamete();
		if (RandomHelper.nextDouble() < 0.5) {
			// randomly mix the gametes as we put gameteL as the left gamete.
			int[] tmp = gameteL;
			gameteL = gameteR;
			gameteR = tmp;
		}
		for (int gene = 0; gene < gameteL.length; gene++) {
			// AA / BB
			if (gameteL[gene] == gameteR[gene]) {
				child.setAllele(gene, gameteL[gene]);
			} else {
				// AB
				if (gameteL[gene] == HOMOZYGOT_AA) {
					child.setAllele(gene, HETEROZYGOT_AB);
				}
				// BA
				else {
					child.setAllele(gene, HETEROZYGOT_BA);
				}
			}
		}

		// mutate
		int numOfMutations = Poisson.staticNextInt(child.getMutationRate());
		for (int i = 0; i < numOfMutations; i++) {
			child.mutate();
		}

		// choose sex
		child.setMale(RandomHelper.nextIntFromTo(0, 1)); // 50% male 50% female

		return child;
	}

	// TODO document

	public abstract double getOutXingTendency(double slope, double maxX,
			double midX, double offset);

	private double getHeterozygotCoefficient() {
		return Params.getHeterozygotCoefficient();
	}

	/**
	 * the gamete returned is haploid therefore is homozygot for all it's
	 * alleles, and the only variance is between A and B alleles. includes
	 * recombination and segregation, as only the <b>"left"</b> gamete is
	 * returned
	 * 
	 * <b>NOTE</b>: this is only to be used with 2 alleles (A and B) and NOT
	 * with 3 alleles (A, B and C) TODO move to 2 allele diploid organism
	 */
	protected static int[] createGamete(int[] alleles) {
		int[] gamete = new int[alleles.length];
		for (int gene = 0; gene < alleles.length; gene++) {
			if (alleles[gene] == HOMOZYGOT_AA || alleles[gene] == HOMOZYGOT_BB) {
				gamete[gene] = alleles[gene];
			} else {
				double rand = RandomHelper.nextDouble();
				// AB
				double threshold = RECOMBINATION_FREQUENCY;
				// BA
				if (alleles[gene] == HETEROZYGOT_BA) {
					threshold = 1 - RECOMBINATION_FREQUENCY;
				}
				if (rand < threshold) {
					// recombination occurred
					gamete[gene] = HOMOZYGOT_BB;
				} else {
					gamete[gene] = HOMOZYGOT_AA;
				}
			}
		}
		return gamete;
	}

	public int[] getGamete() {
		return createGamete(getAlleles());
	}

	public void setOutXingModifier(int outXingModifier) {
		this.outXingModifier = outXingModifier;
	}

	public int getOutXingModifier() {
		return outXingModifier;
	}

	public void setMale(int male) {
		this.male = male;
	}

	public int isMale() {
		return male;
	}

	/**
	 * utility method for counting organisms - i.e., censusing the population
	 * (by aggregateing on it).
	 * 
	 * @return 1 always
	 */
	public int isOutXingOrganism() {
		return 1;
	}

	/*
	 * returns the fraction of heterozygous loci
	 */
	public double getHeterozigosity() {
		double heterozygousCount = 0;
		for (int gene = 0; gene < getAlleles().length; gene++) {
			if (getAllele(gene) == HETEROZYGOT_AB
					|| getAllele(gene) == HETEROZYGOT_BA)
				heterozygousCount++;
		}
		return heterozygousCount / ((double) (getAlleles().length));
	}

	/*
	 * returns the fraction of homozygous deleterious loci in the organism
	 */
	public double getDeleteriousHomozigosity() {
		double homozygousCount = 0;
		for (int gene = 0; gene < getAlleles().length; gene++) {
			if (getAllele(gene) == HOMOZYGOT_BB)
				homozygousCount++;
		}
		return homozygousCount / ((double) getAlleles().length);
	}

	/**
	 * returns 1 for FF, FO, OF and 0 for OO.
	 */
	public int isFAO() {
		if (outXingModifier == HOMOZYGOT_AA)
			return 0;
		else
			return 1;
	}

	public void setMovedToPool(int movedToPool) {
		this.movedToPool = movedToPool;
	}

	public int getMovedToPool() {
		return movedToPool;
	}
}
